using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

using NLog;

using NConf.Interfaces;


namespace NConf.ConfigSources
{
    /// <summary>
    /// A configuration source backed by a SQL database.
    /// </summary>
    public class SQLConfigSource : DictConfigSource
    {
        #region Properties

        ///<summary>
        ///The separator character to use between levels in the configuration hierarchy when reading or writing keys.
        ///</summary>
        public char PathSeparator { get; set; }

        #endregion

        #region Internal Members

        /// Instance of our class-wide logger
        private static Logger logger = LogManager.GetLogger("SQLConfigSource");

		private string connectionString;
		private string storedProcedure;
        private Dictionary<string, object> parameters = null;

        #endregion

        #region Constructors

		public SQLConfigSource(string connectionString, string storedProcedure)
		{
			this.connectionString = connectionString;
			this.storedProcedure = storedProcedure;
            this.parameters = new Dictionary<string, object>();
            this.PathSeparator = '.';
		} // end SQLConfigSource

        #endregion

        #region External API

        /// <summary>
        /// Assign a value to the given parameter of the source SQL query.
        /// </summary>
        /// <param name="name">The name of the parameter to assign.</param>
        /// <param name="value">The value to assign to this parameter.</param>
        public void addParameter(string name, object value)
        {
            parameters[name] = value;
        } // end addParameter

        /// <summary>
		/// Write all configuration values to the config cache.
        /// </summary>
        /// <returns>true if any configuration was loaded, false otherwise.</returns>
		public bool populateCache()
		{
            Configuration = new Dictionary<string, object>();

			logger.Debug("Getting config from database \"{0}\" using stored procedure \"{1}\":", connectionString, storedProcedure);

			using (SqlConnection connection = new SqlConnection(connectionString.Replace("|", ";")))
			{
				SqlCommand command = connection.CreateCommand();
				command.CommandType = CommandType.StoredProcedure;
				command.CommandText = storedProcedure;

                foreach(var kvp in parameters)
                {
                    command.Parameters.AddWithValue(kvp.Key, kvp.Value);
                } // end foreach

				try
				{
					connection.Open();

					SqlDataReader reader = command.ExecuteReader();
					while(reader.Read())
					{
                        cacheNestedValue(reader[0].ToString(), reader[1]);
					} // end while
					reader.Close();

					connection.Close();
				}
				catch(Exception ex)
				{
					logger.ErrorException("Error loading configuration from database!", ex);
				} // end try
			} // end using

			if(Configuration.Count == 0)
			{
				logger.Warn("No configuration loaded from database!");
                return false;
			} // end if

            return true;
		} // end SQLConfigSource

        private void cacheNestedValue(string key, object value)
        {
            Dictionary<string, object> current = Configuration;
            StringBuilder currentPath = new StringBuilder();

            string[] elements = key.Split(PathSeparator);
            string element = null;

            // Iterate through all but the last element, and create dictionaries for each if they don't exist.
            for(int i = 0; i <= elements.Length - 1; i++)
            {
                element = elements[i];

                if(!current.ContainsKey(element))
                {
                    current[element] = new Dictionary<string, object>();
                } // end if

                currentPath.Append(element);
                current = current[element] as Dictionary<string, object>;

                if(current == null)
                {
                    logger.Warn("Couldn't write value {0} at key {1}; the key at {2} already exists, and is not a dictionary!",
                            key,
                            value,
                            currentPath.ToString().Substring(1)
                            );
                    return;
                } // end if
            } // end foreach

            // Use the last element to determine the key we should be writing inside the current dictionary.
            current[elements[elements.Length]] = value;
        } // end cacheNestedValue

        #endregion
    } // end SQLConfigSource
} // end SQLConfigSource
